-
-
Notifications
You must be signed in to change notification settings - Fork 340
feat: Add Grafana module #1509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: Add Grafana module #1509
Conversation
Implements Testcontainers support for Grafana with the following features: - Default Grafana image: grafana/grafana:11.0.0 - Support for custom username/password authentication - Anonymous access configuration - Ability to mount datasources, dashboards, plugins, and notifiers - HTTP health check wait strategy - Connection string generation with embedded credentials 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
✅ Deploy Preview for testcontainers-dotnet ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
4900ecd to
8fa5f1b
Compare
HofmeisterAn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! It looks great overall, but there are a few small things we need to fix before we can merge it.
| /// <summary> | ||
| /// Mounts a datasource configuration file. | ||
| /// </summary> | ||
| /// <param name="datasourceFilePath">The path to the datasource configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithDataSource(string datasourceFilePath) | ||
| { | ||
| return WithBindMount(datasourceFilePath, "/etc/grafana/provisioning/datasources/"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts a dashboard configuration file. | ||
| /// </summary> | ||
| /// <param name="dashboardFilePath">The path to the dashboard configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithDashboard(string dashboardFilePath) | ||
| { | ||
| return WithBindMount(dashboardFilePath, "/etc/grafana/provisioning/dashboards/"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts a plugin configuration file. | ||
| /// </summary> | ||
| /// <param name="pluginFilePath">The path to the plugin configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithPlugin(string pluginFilePath) | ||
| { | ||
| return WithBindMount(pluginFilePath, "/etc/grafana/provisioning/plugins/"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts a notifier configuration file. | ||
| /// </summary> | ||
| /// <param name="notifierFilePath">The path to the notifier configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithNotifier(string notifierFilePath) | ||
| { | ||
| return WithBindMount(notifierFilePath, "/etc/grafana/provisioning/notifiers/"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mounting a share isn't aligned with our best practices. It can make the module incompatible with remote container runtimes. Instead, we recommend using the WithResourceMapping(FileInfo, FileInfo) API, which copies the files into the container.
| .WithPortBinding(GrafanaPort, true) | ||
| .WithUsername(DefaultUsername) | ||
| .WithPassword(DefaultPassword) | ||
| .WithEnvironment("GF_AUTH_ANONYMOUS_ENABLED", "false") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| .WithEnvironment("GF_AUTH_ANONYMOUS_ENABLED", "false") | |
| .WithAnonymousAccessDisabled() |
| /// <summary> | ||
| /// Mounts a datasource configuration file. | ||
| /// </summary> | ||
| /// <param name="datasourceFilePath">The path to the datasource configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithDataSource(string datasourceFilePath) | ||
| { | ||
| return WithBindMount(datasourceFilePath, "/etc/grafana/provisioning/datasources/"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts a dashboard configuration file. | ||
| /// </summary> | ||
| /// <param name="dashboardFilePath">The path to the dashboard configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithDashboard(string dashboardFilePath) | ||
| { | ||
| return WithBindMount(dashboardFilePath, "/etc/grafana/provisioning/dashboards/"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts a plugin configuration file. | ||
| /// </summary> | ||
| /// <param name="pluginFilePath">The path to the plugin configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithPlugin(string pluginFilePath) | ||
| { | ||
| return WithBindMount(pluginFilePath, "/etc/grafana/provisioning/plugins/"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mounts a notifier configuration file. | ||
| /// </summary> | ||
| /// <param name="notifierFilePath">The path to the notifier configuration file.</param> | ||
| /// <returns>A configured instance of <see cref="GrafanaBuilder" />.</returns> | ||
| public GrafanaBuilder WithNotifier(string notifierFilePath) | ||
| { | ||
| return WithBindMount(notifierFilePath, "/etc/grafana/provisioning/notifiers/"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These builder APIs haven't been tested. We typically add builder APIs only for configurations that are essential to run an instance or required for specific use cases.
| public string GetConnectionString() | ||
| { | ||
| var endpoint = GetHttpEndpoint(); | ||
| var username = _configuration.Username ?? GrafanaBuilder.DefaultUsername; | ||
| var password = _configuration.Password ?? GrafanaBuilder.DefaultPassword; | ||
| return new UriBuilder(endpoint) | ||
| { | ||
| UserName = username, | ||
| Password = password | ||
| }.ToString(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this? The tests only use the HTTP header for authentication.
Summary by CodeRabbit
WalkthroughThis PR introduces Grafana container support to Testcontainers by adding a new module with a builder for configuring containers, setting credentials, enabling anonymous access, and provisioning assets. The solution is extended with the new projects, documentation is updated, and comprehensive tests validate container lifecycle and HTTP interactions. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Builder as GrafanaBuilder
participant Container as GrafanaContainer
participant Docker as Docker Engine
participant Grafana as Grafana Service
User->>Builder: new GrafanaBuilder()
Builder->>Builder: Init() - set defaults
Note over Builder: image: 11.0.0<br/>port: 3000<br/>credentials: admin/admin
User->>Builder: WithUsername("custom")
User->>Builder: WithPassword("secret")
User->>Builder: Build()
Builder->>Container: create(GrafanaConfiguration)
Container->>Docker: create container
Docker->>Grafana: spin up
rect rgb(200, 220, 240)
Note over Docker,Grafana: Health Check Wait Strategy
Grafana-->>Docker: health endpoint ready
end
User->>Container: GetHttpEndpoint()
Container-->>User: http://localhost:3000
User->>Container: GetConnectionString()
Container-->>User: http://custom:secret@localhost:3000
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
src/Testcontainers.Grafana/GrafanaContainer.cs (1)
32-42: Reconsider the need for this method given authentication patterns.The
GetConnectionString()method embeds credentials in the URI, but this approach has several concerns:
- Security: Embedding credentials in URIs is problematic as they may be logged, cached, or exposed in stack traces.
- Grafana authentication: Modern Grafana typically uses API keys or Bearer tokens in HTTP headers, not HTTP Basic Auth via URI.
- Unused pattern: As noted in the previous review, tests authenticate using HTTP headers, suggesting this method may not align with actual usage patterns.
If this method isn't required by consumers or tests, consider removing it. Otherwise, clarify its intended use case.
If the method must remain, consider documenting its purpose and warning about security implications:
/// <summary> /// Gets the Grafana connection string. /// </summary> + /// <remarks> + /// This method embeds credentials in the URI for backward compatibility. + /// For production use, prefer API keys with the Authorization header. + /// Avoid logging or caching the returned connection string as it contains credentials. + /// </remarks> /// <returns>The Grafana connection string.</returns> public string GetConnectionString()Alternatively, if tests and typical usage rely on HTTP header authentication, remove this method:
- /// <summary> - /// Gets the Grafana connection string. - /// </summary> - /// <returns>The Grafana connection string.</returns> - public string GetConnectionString() - { - var endpoint = GetHttpEndpoint(); - var username = _configuration.Username ?? GrafanaBuilder.DefaultUsername; - var password = _configuration.Password ?? GrafanaBuilder.DefaultPassword; - return new UriBuilder(endpoint) - { - UserName = username, - Password = password - }.ToString(); - }src/Testcontainers.Grafana/GrafanaBuilder.cs (2)
83-116: Replace WithBindMount with WithResourceMapping for better compatibility.As noted in previous reviews, mounting shares with
WithBindMountcan cause incompatibility with remote container runtimes. The recommended approach is to useWithResourceMapping(FileInfo, FileInfo)to copy files into the container.Additionally, these provisioning methods lack test coverage. Consider adding tests or documenting the expected usage patterns.
Based on past review feedback.
133-133: Use WithAnonymousAccessDisabled() for consistency.As noted in previous reviews, line 133 should use the
WithAnonymousAccessDisabled()method instead of directly setting the environment variable.Apply this diff:
.WithUsername(DefaultUsername) .WithPassword(DefaultPassword) - .WithEnvironment("GF_AUTH_ANONYMOUS_ENABLED", "false") + .WithAnonymousAccessDisabled() .WithEnvironment("GF_AUTH_BASIC_ENABLED", "true")Based on past review feedback.
🧹 Nitpick comments (6)
tests/Testcontainers.Grafana.Tests/Testcontainers.Grafana.Tests.csproj (1)
3-3: Consider multi-targeting for broader test coverage.The test project targets only
net9.0, whereas the main Grafana project supportsnet8.0;net9.0;netstandard2.0;netstandard2.1. While this may be intentional for simplicity, testing against multiple frameworks ensures compatibility across the supported target frameworks.If broader coverage is desired, apply this diff:
- <TargetFrameworks>net9.0</TargetFrameworks> + <TargetFrameworks>net8.0;net9.0</TargetFrameworks>tests/Testcontainers.Grafana.Tests/GrafanaContainerTest.cs (5)
27-28: Consider removing ConfigureAwait(true).In test code,
ConfigureAwait(true)is redundant since that's the default behavior. Omitting it simplifies the code without changing functionality.Apply this diff to simplify:
- using var httpResponse = await httpClient.GetAsync("api/health", TestContext.Current.CancellationToken) - .ConfigureAwait(true); + using var httpResponse = await httpClient.GetAsync("api/health", TestContext.Current.CancellationToken);
46-47: Consider removing ConfigureAwait(true).Same as the previous test,
ConfigureAwait(true)is redundant in test code.Apply this diff:
- using var httpResponse = await httpClient.GetAsync("api/org", TestContext.Current.CancellationToken) - .ConfigureAwait(true); + using var httpResponse = await httpClient.GetAsync("api/org", TestContext.Current.CancellationToken);
53-65: Consider validating the URI format.While the current assertions verify that key components are present in the connection string, they don't validate that the connection string is a well-formed URI with credentials properly encoded.
Consider adding a URI parsing assertion:
public void GetConnectionStringReturnsExpectedFormat() { // When var connectionString = _grafanaContainer.GetConnectionString(); // Then Assert.NotNull(connectionString); + Assert.True(Uri.TryCreate(connectionString, UriKind.Absolute, out var uri), "Connection string should be a valid URI"); Assert.Contains(GrafanaBuilder.DefaultUsername, connectionString); Assert.Contains(GrafanaBuilder.DefaultPassword, connectionString); Assert.Contains(_grafanaContainer.Hostname, connectionString); }
97-98: Consider removing ConfigureAwait(true).Consistent with previous suggestions,
ConfigureAwait(true)is redundant in test code.Apply this diff:
- using var httpResponse = await httpClient.GetAsync("api/org", TestContext.Current.CancellationToken) - .ConfigureAwait(true); + using var httpResponse = await httpClient.GetAsync("api/org", TestContext.Current.CancellationToken);
131-132: Consider removing ConfigureAwait(true).Consistent with previous suggestions,
ConfigureAwait(true)is redundant in test code.Apply this diff:
- using var httpResponse = await httpClient.GetAsync("api/org", TestContext.Current.CancellationToken) - .ConfigureAwait(true); + using var httpResponse = await httpClient.GetAsync("api/org", TestContext.Current.CancellationToken);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
Testcontainers.sln(6 hunks)docs/modules/index.md(1 hunks)src/Testcontainers.Grafana/GrafanaBuilder.cs(1 hunks)src/Testcontainers.Grafana/GrafanaConfiguration.cs(1 hunks)src/Testcontainers.Grafana/GrafanaContainer.cs(1 hunks)src/Testcontainers.Grafana/Testcontainers.Grafana.csproj(1 hunks)src/Testcontainers.Grafana/Usings.cs(1 hunks)tests/Testcontainers.Grafana.Tests/.runs-on(1 hunks)tests/Testcontainers.Grafana.Tests/GrafanaContainerTest.cs(1 hunks)tests/Testcontainers.Grafana.Tests/Testcontainers.Grafana.Tests.csproj(1 hunks)tests/Testcontainers.Grafana.Tests/Usings.cs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tests/Testcontainers.Grafana.Tests/GrafanaContainerTest.cs (2)
src/Testcontainers.Grafana/GrafanaBuilder.cs (15)
GrafanaContainer(119-123)GrafanaBuilder(18-22)GrafanaBuilder(28-32)GrafanaBuilder(42-46)GrafanaBuilder(53-57)GrafanaBuilder(63-66)GrafanaBuilder(72-76)GrafanaBuilder(83-86)GrafanaBuilder(93-96)GrafanaBuilder(103-106)GrafanaBuilder(113-116)GrafanaBuilder(126-140)GrafanaBuilder(143-146)GrafanaBuilder(149-152)GrafanaBuilder(155-158)src/Testcontainers.Grafana/GrafanaContainer.cs (3)
GrafanaContainer(13-17)GetHttpEndpoint(23-26)GetConnectionString(32-42)
src/Testcontainers.Grafana/GrafanaBuilder.cs (2)
src/Testcontainers.Grafana/GrafanaConfiguration.cs (6)
PublicAPI(4-71)GrafanaConfiguration(12-18)GrafanaConfiguration(24-28)GrafanaConfiguration(34-38)GrafanaConfiguration(44-48)GrafanaConfiguration(55-60)src/Testcontainers.Grafana/GrafanaContainer.cs (2)
PublicAPI(4-43)GrafanaContainer(13-17)
src/Testcontainers.Grafana/GrafanaConfiguration.cs (2)
src/Testcontainers.Grafana/GrafanaBuilder.cs (1)
PublicAPI(4-159)src/Testcontainers.Grafana/GrafanaContainer.cs (1)
PublicAPI(4-43)
src/Testcontainers.Grafana/GrafanaContainer.cs (2)
src/Testcontainers.Grafana/GrafanaBuilder.cs (16)
PublicAPI(4-159)GrafanaContainer(119-123)GrafanaBuilder(18-22)GrafanaBuilder(28-32)GrafanaBuilder(42-46)GrafanaBuilder(53-57)GrafanaBuilder(63-66)GrafanaBuilder(72-76)GrafanaBuilder(83-86)GrafanaBuilder(93-96)GrafanaBuilder(103-106)GrafanaBuilder(113-116)GrafanaBuilder(126-140)GrafanaBuilder(143-146)GrafanaBuilder(149-152)GrafanaBuilder(155-158)src/Testcontainers.Grafana/GrafanaConfiguration.cs (6)
PublicAPI(4-71)GrafanaConfiguration(12-18)GrafanaConfiguration(24-28)GrafanaConfiguration(34-38)GrafanaConfiguration(44-48)GrafanaConfiguration(55-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci (Testcontainers, ubuntu-24.04)
- GitHub Check: analyze (csharp)
🔇 Additional comments (21)
tests/Testcontainers.Grafana.Tests/.runs-on (1)
1-1: LGTM!The runner specification is appropriate for Docker-based Grafana container tests.
src/Testcontainers.Grafana/Usings.cs (1)
1-8: LGTM!The global usings are appropriate for the Grafana module and follow the standard pattern used across other Testcontainers modules.
tests/Testcontainers.Grafana.Tests/Usings.cs (1)
1-6: LGTM!The global usings are appropriate for the Grafana test project.
src/Testcontainers.Grafana/Testcontainers.Grafana.csproj (1)
1-12: LGTM!The project configuration follows the standard pattern for Testcontainers modules with appropriate multi-targeting and dependencies.
docs/modules/index.md (1)
45-45: LGTM!The Grafana module entry is correctly formatted and positioned alphabetically in the modules table.
Testcontainers.sln (1)
65-66: LGTM!The solution file correctly adds both Grafana projects with proper build configurations and folder nesting, following the established solution structure.
Also applies to: 185-186, 365-368, 605-608, 812-812, 872-872
src/Testcontainers.Grafana/GrafanaContainer.cs (2)
9-17: LGTM!The constructor follows the standard pattern for Testcontainers modules.
19-26: LGTM!The
GetHttpEndpoint()method correctly constructs the Grafana HTTP endpoint using the mapped port.tests/Testcontainers.Grafana.Tests/GrafanaContainerTest.cs (3)
3-16: LGTM!The test class setup correctly implements IAsyncLifetime for proper container lifecycle management. The container is appropriately shared across tests in this class.
67-83: LGTM!The nested test class properly isolates custom credential testing with its own container instance. The IAsyncLifetime implementation ensures proper resource cleanup.
105-120: LGTM!The nested test class properly validates anonymous access configuration with its own isolated container instance.
src/Testcontainers.Grafana/GrafanaConfiguration.cs (4)
12-18: LGTM!The basic constructor correctly initializes the configuration with optional username and password parameters.
55-60: LGTM!The merge constructor correctly implements the immutable copy pattern, preferring new values when present and falling back to old values when null.
65-70: LGTM!The get-only properties enforce immutability as expected.
24-38: ****The review comment incorrectly identifies a credential loss issue. The code properly preserves
UsernameandPasswordduring cloning.When
Clone()is called, it creates a newGrafanaConfigurationvia the copy constructors (lines 24-28 or 34-38), which intentionally have null properties. This is then passed to theMergemethod, which invokes the merge constructor (lines 54-62). The merge constructor explicitly preserves credentials from the old configuration using null-coalescing operators:Username = newValue.Username ?? oldValue.Username; Password = newValue.Password ?? oldValue.Password;Since
DockerResourceConfigurationcontains the previously configured credentials and serves asoldValuein the merge operation, credentials are retained when cloning. This pattern is consistent across all container modules in the codebase, confirming it is intentional framework design.Likely an incorrect or invalid review comment.
src/Testcontainers.Grafana/GrafanaBuilder.cs (6)
18-32: LGTM!The constructors correctly implement the builder pattern with proper initialization.
42-57: LGTM!The credential configuration methods correctly implement the fluent, immutable builder pattern and set appropriate Grafana environment variables.
72-76: LGTM! Note the admin role for anonymous access.The anonymous access methods are correctly implemented. Note that enabling anonymous access grants Admin role to anonymous users, which is appropriate for testing scenarios but should be documented for production use.
119-123: LGTM!The Build method correctly validates configuration before constructing the container.
143-158: LGTM!The Clone and Merge methods correctly implement the immutable builder pattern. Note that the Clone methods may not preserve Username/Password properties, which was flagged in the GrafanaConfiguration.cs review.
7-7: ---Update Grafana image to 12.3.0 or confirm 11.0.0 is required.
The image is pinned to version 11.0.0, but the latest stable version is 12.3.0. If there's no specific reason to remain on 11.0.0 (e.g., testing backward compatibility), consider updating to the current stable release.
Implements Testcontainers support for Grafana with the following features: